package cn.tqfeiyang.springplus.framework.security;

import cn.tqfeiyang.springplus.framework.Constants;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.beans.factory.annotation.Qualifier;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;
import java.util.Collection;

/**
 * 认证过滤器
 *
 * 需要注意的是，全局异常处理只能捕获 Controller 层及之后的异常，如果 Filter 中出现异常，必须在 Filter 中处理异常。
 * 你可以在 Filter 中使用 try-catch 语句来处理异常，自定义返回的错误信息。
 *
 * @author tqfeiyang
 * @since 2024-07-22
 */
@Component
public class AuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private JwtTokenService jwtTokenService;

    @Resource
    private AuthenticationUserLoader authenticationUserLoader;

    @Resource
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver handlerExceptionResolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        try {
            // JWT Token is in the form "Bearer token"
            String token = extractTokenFromRequest(request);

            if (StringUtils.hasText(token)) {
                JwtTokenSubject subject = jwtTokenService.parse(token);
                AuthenticationUserDetails userDetails = authenticationUserLoader.loadByUsername(subject.getUsername());
                if (userDetails == null) {
                    throw new JwtException("Username not found");
                }

                Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, authorities);

                // After setting the Authentication in the context, we specify
                // that the current user is authenticated. So it passes the
                // Spring Security Configurations successfully.
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                // Set the subject so that in controller we will be using it to create new JWT
                if (isRefreshTokenRequest(request)) {
                    request.setAttribute(Constants.AUTH_SUBJECT_ATTR, subject);
                }
            }

            chain.doFilter(request, response);

        } catch (ExpiredJwtException ex) {
            if (isRefreshTokenRequest(request)) {
                // create a UsernamePasswordAuthenticationToken with null values.
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(null, null, null);

                // After setting the Authentication in the context, we specify
                // that the current user is authenticated. So it passes the
                // Spring Security Configurations successfully.
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                // Set the subject so that in controller we will be using it to create new JWT
                request.setAttribute(Constants.AUTH_SUBJECT_ATTR, jwtTokenService.parse(ex.getClaims()));

                chain.doFilter(request, response);
            } else {
                handlerExceptionResolver.resolveException(request, response, null, ex);
            }
        } catch (Exception ex) {
            handlerExceptionResolver.resolveException(request, response, null, ex);
        }
    }

    private String extractTokenFromRequest(HttpServletRequest request) {
        // 读取请求头 Authorization = Bearer {{token}}
        String bearerToken = request.getHeader(Constants.AUTH_TOKEN_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.AUTH_BEARER_PREFIX)) {
            return bearerToken.substring(7, bearerToken.length());
        }

        // 读取请求参数 ?token={{token}}
        String parameterValue = request.getParameter(Constants.AUTH_TOKEN_PARAM);
        if (StringUtils.hasText(parameterValue)) {
            return parameterValue;
        }

        return null;
    }

    private boolean isRefreshTokenRequest(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        return requestURL.contains(Constants.AUTH_REFRESH_URL);
    }
}
